using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.ServiceModel;
using System.ServiceModel.Channels;
using InfiniTec.Exchange.Notifications.Microsoft.Exchange.WebServices;

namespace InfiniTec.Exchange.Notifications
{
	/// <summary>
	/// Default implementation for the <see cref="IExchangeWebServicesAdapter"/> interface. Uses an Exchange 2007 SP1 WebService proxy and
	/// WCF to manage subscriptions.
	/// </summary>
	public sealed class ExchangeWebServicesAdapter : IExchangeWebServicesAdapter, IDisposable
	{
		private ChannelFactory<ExchangeServicePortTypeChannel> _ExchangeServiceChannelFactory;

		/// <summary>
		/// Gets impersonation settings used to specify which users account to use when executing Exchange WebService calls.
		/// </summary>
		/// <value>The impersonation settings used by this instance.</value>
		public ImpersonationSettings ImpersonationSettings
		{
			get; private set;
		}

		/// <summary>
		/// Gets or sets a value indicating whether subscriptions can be bound to public folders. This requires Exchange 2007 Service Pack 1 or later.
		/// </summary>
		/// <value>
		/// 	<c>true</c> if subscriptions can be bound to public folders; otherwise, <c>false</c>.
		/// </value>
		public ExchangeVersion RequestedExchangeVersion { get; set; }

		/// <summary>
		/// Initializes a new instance of the <see cref="ExchangeWebServicesAdapter"/> class. Uses integrated authentication.
		/// </summary>
		/// <param name="exchangeServerUrl">An url pointing to the CAS role of an Exchange 2007 or later server.</param>
		public ExchangeWebServicesAdapter(Uri exchangeServerUrl) : this(exchangeServerUrl, null)
		{
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="ExchangeWebServicesAdapter"/> class.
		/// </summary>
		/// <param name="exchangeServerUrl">An url pointing to the CAS role of an Exchange 2007 or later server.</param>
		/// <param name="credential">The credential to use.</param>
		public ExchangeWebServicesAdapter(Uri exchangeServerUrl, NetworkCredential credential)
		{
			if (exchangeServerUrl == null) throw new ArgumentNullException("exchangeServerUrl");
			ExchangeServerUrl = exchangeServerUrl;
			Credential = credential;
			ImpersonationSettings = new ImpersonationSettings();
			CreateWcfChannelFactory();
		}

		/// <summary>
		/// Gets or sets the credential to use when managing subscriptions.
		/// </summary>
		/// <value>The credential.</value>
		public NetworkCredential Credential { get; set; }
		/// <summary>
		/// Gets or sets the url pointing to the CAS role of an Exchange 2007 or later server.
		/// </summary>
		/// <example>
		/// adapter.ExchangeServerUrl = "https://casserver/ews/exchange.asmx";
		/// </example>
		/// <value>The exchange server URL.</value>
		private Uri ExchangeServerUrl { get; set; }

		/// <summary>
		/// Creates a subscription on an Exchange 2007 server.
		/// </summary>
		/// <param name="folders">A list of references to folders to monitor for changes.</param>
		/// <param name="events">A valid combination of the <see cref="EventTypes"/> values, except for <see cref="EventTypes.None"/>.</param>
		/// <param name="lastKnownWatermark">The last known watermark. Exchange will resent changes which occured after the date specified by the watermark. A watermark is valid for about 30 days (Exchange 2007).</param>
		/// <param name="timeoutIntervalInMinutes">The timeout interval in minutes. A subscriptions is to be considered timed-out when no status message or other event happend for a timespan twice as long as the timeout interval.</param>
		/// <param name="localEndpoint">The local endpoint of a <see cref="PushNotificationListener"/>.</param>
		/// <returns>
		/// A <see cref="SubscriptionInfo"/> containing the subscription id and an initial watermark.
		/// </returns>
		SubscriptionInfo IExchangeWebServicesAdapter.Subscribe(IEnumerable<FolderReference> folders, EventTypes events, string lastKnownWatermark,
								int timeoutIntervalInMinutes, Uri localEndpoint)
		{
			if (folders == null) throw new ArgumentNullException("folders");
			if (localEndpoint == null) throw new ArgumentNullException("localEndpoint");
			var subscriptionRequestType = new PushSubscriptionRequestType
			           {
			           EventTypes = ConvertEventsToSubscribe(events),
			           FolderIds = folders.Select(reference => reference.ToEwsFolderId()).ToArray(),
			           StatusFrequency = timeoutIntervalInMinutes,
			           URL = localEndpoint.ToString(),
			           Watermark = lastKnownWatermark
			           };

			return CreateSubscription(subscriptionRequestType);
		}

		private SubscriptionInfo CreateSubscription(BaseSubscriptionRequestType subscriptionRequestType)
		{
			var request = new SubscribeRequest
			              {
			              Subscribe = new SubscribeType
			                          {
			                          Item = subscriptionRequestType
			                          },
			              ExchangeImpersonation = ImpersonationSettings.ToExchangeImpersonationType()
			              };
			if (RequestedExchangeVersion == ExchangeVersion.Exchange2007WithServicePack1)
			{
				request.RequestServerVersion = new RequestServerVersion
				                               {
													Version = ExchangeVersionType.Exchange2007_SP1
				                               };
			}

			var response = ExecuteExchangeMethod(service => service.Subscribe(request));
			
			var subscriptionResponse = ((SubscribeResponseMessageType) response.SubscribeResponse1.ResponseMessages.Items[0]);
			
			subscriptionResponse.EvaluateAndThrowIfNecessary();

			return new SubscriptionInfo(subscriptionResponse.SubscriptionId, subscriptionResponse.Watermark);
		}

		SubscriptionInfo IExchangeWebServicesAdapter.Subscribe(IEnumerable<FolderReference> folders, EventTypes events, string lastKnownWatermark, int timeoutIntervalInMinutes)
		{
			if (folders == null) throw new ArgumentNullException("folders");
			var subscriptionRequest = new PullSubscriptionRequestType
									  {
										  EventTypes = ConvertEventsToSubscribe(events),
										  FolderIds = folders.Select(reference => reference.ToEwsFolderId()).ToArray(),
										  Watermark = lastKnownWatermark,
										  Timeout = timeoutIntervalInMinutes
									  };
			return CreateSubscription(subscriptionRequest);
		}

		void IExchangeWebServicesAdapter.Unsubscribe(string subscriptionId)
		{
			var request = new UnsubscribeRequest
			              {
				Unsubscribe = new UnsubscribeType
							  {
								  SubscriptionId = subscriptionId
							  },
				ExchangeImpersonation = ImpersonationSettings.ToExchangeImpersonationType()
			};
			if (RequestedExchangeVersion == ExchangeVersion.Exchange2007WithServicePack1)
			{
				request.RequestServerVersion = new RequestServerVersion
				{
					Version = ExchangeVersionType.Exchange2007_SP1
				};
			}

			var response = ExecuteExchangeMethod(service => service.Unsubscribe(request));
			var subscriptionResponse = response.UnsubscribeResponse1.ResponseMessages.Items[0];
			subscriptionResponse.EvaluateAndThrowIfNecessary();
		}

		IEnumerable<EventInfo> IExchangeWebServicesAdapter.GetEvents(string subscriptionId, string watermark)
		{
			if (string.IsNullOrEmpty(watermark)) throw new ArgumentNullException("watermark");
			if (string.IsNullOrEmpty(subscriptionId)) throw new ArgumentNullException("subscriptionId");
			var request = new GetEventsRequest
						  {
							  GetEvents = new GetEventsType
										  {
											  SubscriptionId = subscriptionId,
											  Watermark = watermark
										  },
							  ExchangeImpersonation = ImpersonationSettings.ToExchangeImpersonationType()
						  };
			if (RequestedExchangeVersion == ExchangeVersion.Exchange2007WithServicePack1)
			{
				request.RequestServerVersion = new RequestServerVersion
				{
					Version = ExchangeVersionType.Exchange2007_SP1
				};
			}

			var response = ExecuteExchangeMethod(service => service.GetEvents(request));
			
			var subscriptionResponse = ((GetEventsResponseMessageType) response.GetEventsResponse1.ResponseMessages.Items[0]);
			subscriptionResponse.EvaluateAndThrowIfNecessary();
			return subscriptionResponse.Notification.ConvertEvents();
		}


		/// <summary>
		/// Creates the WCF channel factory which is used to call into the Exchange 2007 server.
		/// </summary>
		private void CreateWcfChannelFactory()
		{
			Binding binding = new BasicHttpBinding(ExchangeServerUrl.Scheme == Uri.UriSchemeHttps
				                     	? BasicHttpSecurityMode.Transport
				                     	: BasicHttpSecurityMode.None);
			var customBinding = new CustomBinding(binding);

			customBinding.Elements.Find<HttpTransportBindingElement>().AuthenticationScheme = (Credential ==
			                                                                                   CredentialCache.
			                                                                                   	DefaultNetworkCredentials)
			                                                                                  	? AuthenticationSchemes.
			                                                                                  	  	IntegratedWindowsAuthentication
			                                                                                  	: AuthenticationSchemes.Negotiate;

			_ExchangeServiceChannelFactory = new ChannelFactory<ExchangeServicePortTypeChannel>(
				customBinding, ExchangeServerUrl.ToString());
			_ExchangeServiceChannelFactory.Credentials.Windows.ClientCredential = Credential;
			_ExchangeServiceChannelFactory.Credentials.Windows.AllowNtlm = true;
		}

		private static NotificationEventTypeType[] ConvertEventsToSubscribe(EventTypes events)
		{
			var result = new List<NotificationEventTypeType>();

			if ((events & EventTypes.Copied) == EventTypes.Copied) result.Add(NotificationEventTypeType.CopiedEvent);
			if ((events & EventTypes.Created) == EventTypes.Created) result.Add(NotificationEventTypeType.CreatedEvent);
			if ((events & EventTypes.Deleted) == EventTypes.Deleted) result.Add(NotificationEventTypeType.DeletedEvent);
			if ((events & EventTypes.Modified) == EventTypes.Modified) result.Add(NotificationEventTypeType.ModifiedEvent);
			if ((events & EventTypes.Moved) == EventTypes.Moved) result.Add(NotificationEventTypeType.MovedEvent);
			if ((events & EventTypes.NewMail) == EventTypes.NewMail) result.Add(NotificationEventTypeType.NewMailEvent);

			return result.ToArray();
		}

		/// <summary>
		/// Executes the exchange method and performs some clean-up operations in case of a failure.
		/// </summary>
		/// <typeparam name="TReturn">The type of the return.</typeparam>
		/// <param name="code">The code to execute.</param>
		/// <returns>The value returned from the WebService call.</returns>
		private TReturn ExecuteExchangeMethod<TReturn>(Func<ExchangeServicePortType, TReturn> code)
		{
			ExchangeServicePortTypeChannel channel = _ExchangeServiceChannelFactory.CreateChannel();

			bool error = true;
			try
			{
				TReturn result = code((ExchangeServicePortType) channel);
				channel.Close();
				error = false;
				return result;
			}
			finally
			{
				if (error)
				{
					channel.Abort();
				}
			}
		}

		public void Dispose()
		{
			if (_ExchangeServiceChannelFactory != null)
			{
				_ExchangeServiceChannelFactory.Close();
			}
		}
	}
}